fix: table toolbar, header columns, click-to-select, wrap and auto-fit (#556) - #557
Conversation
…ct cells (frappe#556) Combines the separate Edit Table / Table Actions controls into one icon-only Table menu, unifies table- and cell-level text colour into a single Text Colour control, adds Fill and Border to match Shape formatting, and moves Delete to the end of the toolbar. Adds header columns alongside header rows, independently configurable. A single click on a picked table now selects a cell instead of opening it for editing — only a double click opens it — with click+drag range selection and click-outside deselection unchanged. Adds a font selector for table cell text, reusing the shared Espresso font list now extracted into diagram/textFonts.js. Auto-fit on double-clicking a resize handle and cell text wrapping (no scrollbars, row auto-grow) are not yet in this PR — tracked as the remaining slices of frappe#556. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#556) Lint: canMerge/canSplit in WhiteboardTable.vue and extended in startCellRangeDrag were dead once showRange stopped reading them and the click-vs-drag release branch was removed. E2E: table-cell-text.spec.js opened a cell with two separate clicks (the old T2 path); a plain click now only selects, so opening a cell needs a double click. Also repoints the cell-colour test at the unified Text Colour control ("Cell text colour" was folded into it). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… auto-fit (frappe#556) A cell now wraps its text instead of scrolling horizontally, and its row grows live as it's typed into — mirroring WhiteboardStickyNote's growToText/commit split (unrecorded growth while typing, the final size landing with the text in one undo step). Enter inserts a line break instead of committing, reusing the same newlineIntent list-continuation the sticky note uses; commit now happens on click-away, matching how a sticky note behaves. Adds a mark-aware run wrapper (wrapRuns, richText.js) so a wrapped cell keeps its bold/italic/underline/strike marks on the exact characters they belonged to — including the original whitespace's own marks, not a reconstructed space that could silently move a formatting boundary. The committed SVG render and the export mirror the same wrapped, marked lines. Double-clicking a column or row's resize handle auto-fits that dimension to its content, reusing the same undo labels a manual drag already uses. lineBeforeCaret/deleteBeforeCaret move from WhiteboardStickyNote.vue into richTextDom.js so the table cell editor can reuse them rather than holding a second copy. Known gap, not introduced by this change: clicking empty canvas does not close/deselect an open table on a UNIFIED document (most new diagrams) — the select tool's empty-click path there (useSelection.js) never reaches the whiteboard UI's own clearSelection, which is what actually clears editingCell/ cellRange. Reproduces identically on main before this branch's changes, so it predates frappe#556 entirely. Filing separately rather than risking a rushed fix to shared click-dispatch code shared by every whiteboard object type, not just tables. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#556) Enter now inserts a line break in a wrapping cell instead of closing it, so table-cell-text.spec.js's commit step (previously page.keyboard.press('Enter')) never landed. Replaced with a click on the seeded table's neighbouring cell, which reliably commits (startCellRangeDrag nulls editingCell on every press, whichever cell it lands on) without depending on the separate, currently- broken empty-canvas deselect path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
frappe#556) boxInWindow(page, cell, ...) returns the "CELL-TEXT" <text> element's own rendered bounding box — as wide as the glyphs (~60-90px) — not the table cell's (120-160px in these fixtures). A 1.5x multiple of that box never left the originating cell, so the click landed on the still-open editor's own div (pointerdown.stop) instead of the table underneath, and nothing committed. Fixed to a flat 200px offset from the text's left edge, which clears both fixtures' cell widths into the neighbouring column. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…up (frappe#556) Feedback on the combined Table control: - Its icon (lucide-table) was the same one the Table INSERT tool and the toolbar's Table trigger area already use — swapped to lucide-table-properties so it reads as "settings", not "insert" or "the Merge icon's neighbour". - Moved it next to Merge/Split by rendering TableCellGroup before WhiteboardObjectGroup in CanvasToolbar.vue, so the cell group's trailing Merge/Split sits immediately before the object group's leading Table control — both reshape the table, just gated differently. Delete stays the true last item, at the end of the object group either way. - The popup ran off the bottom of the screen: a single narrow (176px) column stacked Rows/Columns/header checkboxes/alignment/the action menu one item per line. Widened to 320px (w-72, matching TableOptions) and laid Rows + Columns, the two header checkboxes, and the action menu's own groups out side by side, roughly halving the popup's height. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vibhavkatre
left a comment
There was a problem hiding this comment.
Reviewed the whole branch against the source. The toolbar consolidation, header columns, click-to-select and double-click auto-fit all hold up, and the mark-aware wrapRuns is a careful piece of work — keeping the original whitespace character instead of reconstructing it is the right call. One confirmed defect before this can merge.
The sizing wrap and the render wrap are not the same algorithm
richText.js says wrapCharLine is the "same algorithm as textMetrics.js's wrapLines, so a cell wraps exactly where the height that was measured for it expects it to". They diverge on consecutive whitespace, and the render always produces the extra line:
wrapLinessplits on/\s+/and drops empties, so a run of whitespace costs one column.wrapCharLinekeeps every original whitespace character as its own token, so a run of n whitespace characters costs n columns.
The sizing path — wrappedCellLines → wrappedCellHeight, which feeds growTableRow, flushDraft and autoFitRowHeight — goes through wrapLines. The canvas render (wrappedCellRunLines) and the export (useThumbnail.js) go through wrapRuns. So a cell holding two adjacent spaces is grown for fewer lines than it is drawn with, and the last line falls outside the row.
Reproduces on a default table (TABLE_CELL_W 120, TABLE_FONT_SIZE 14, so perLine is 12):
"Total: 1,240"
row grown for 1 line : ["Total: 1,240"]
canvas draws 2 lines: ["Total: ", "1,240"]
"one two three four" (perLine 20)
row grown for 1 line : ["one two three four"]
canvas draws 2 lines: ["one two three ", "four"]
Two spaces after a full stop is ordinary typing, so this is reachable without anything unusual in the cell. trimRuns does not help — it only trims the ends, and the divergence is interior.
The intent behind wrapCharLine is right, so the fix probably belongs on the sizing side: measure with the same per-character tokenizer rather than with wrapLines, so both paths count whitespace the same way. Then the comment's claim becomes true, and the export, the canvas and the row height agree by construction rather than by coincidence.
Worth a test that pins the two paths against each other over text with double spaces, so they cannot drift apart again.
Minor
The known gap you flagged in the commit message — clicking empty canvas does not deselect an open table on a unified document — has no issue filed for it that I can find. Please file it so it is not lost.
Verified, no action needed
ItemListRowandPopover'stoggledefault-slot prop both exist in frappe-ui 1.0.0-beta.19.borderinWhiteboardObjectGroupcannot beundefined:hasSelectioniscells.length > 0, sofirstCellis always present when it is true.charsPerLineclamps to 1, so a 24px column cannot drivewrapCharLine's long-word loop into a hang.- No unused imports left after the refactor.
…ppe#556, frappe#557) wrappedCellLines sized a row's height with textMetrics.js's wrapLines, which collapses a whole run of whitespace to one column via /\s+/ — but the canvas render and export (wrappedCellRunLines, via richText.js's wrapRuns) count every whitespace character as its own column. A cell with a run of two or more consecutive spaces was grown for fewer lines than it was drawn with, so its last line fell outside the row. wrappedCellLines now goes through the same per-character wrapRuns tokenizer, so sizing and rendering agree by construction. Filed frappe#563 for the separate empty-canvas-deselect gap the review also flagged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confidence Score: 4/5The canceled-edit row-growth defect should be fixed before merging. Typing can mutate row geometry outside history, while Escape discards only the text draft and leaves that geometry applied. Files Needing Attention: frontend/src/composables/useTableCellEditor.js Prompt To Fix All With AI### Issue 1
frontend/src/composables/useTableCellEditor.js:118-120
**Escape preserves row growth**
When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded `growTableRow` mutation is not reverted, leaving the row permanently enlarged without an undo entry.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/table-toolb..." | Re-trigger Greptile |
| updateLineCount(draftCell.value, runs) | ||
| const current = toValue(table) | ||
| store.growTableRow(current.id, draftCell.value.row, measuredHeight(draftCell.value, runs)) |
There was a problem hiding this comment.
When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded growTableRow mutation is not reverted, leaving the row permanently enlarged without an undo entry.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/composables/useTableCellEditor.js
Line: 118-120
Comment:
**Escape preserves row growth**
When typing grows a table row and the user presses Escape, the text draft is discarded but the unrecorded `growTableRow` mutation is not reverted, leaving the row permanently enlarged without an undo entry.
**Knowledge Base Used:**
- [Canvas interaction and rendering](https://app.greptile.com/frappe/-/custom-context/knowledge-base/frappe/draw/-/docs/canvas-interaction-and-rendering.md)
- [Whiteboard and freeform content](https://app.greptile.com/frappe/-/custom-context/knowledge-base/frappe/draw/-/docs/whiteboard-and-freeform-content.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
vibhavkatre
left a comment
There was a problem hiding this comment.
The wrap defect is fixed at the right layer. wrappedCellLines now goes through wrapRuns' per-character tokenizer, so sizing and rendering share one definition of what a run of whitespace costs instead of two that disagreed — wrapLines' /\s+/ charging one column where the render charged one per character. Sizing and drawing now agree by construction rather than by coincidence, which is the fix I was after.
The new test compares the two paths directly on the repro ('one two three four' at 120px), so it fails if they ever diverge again. The comments in both files were updated to describe the new arrangement rather than left pointing at the old one.
Merging.
Summary
All 18 items of #556 except one known pre-existing gap (see below):
Known gap — not introduced by this branch
Clicking empty canvas does not close/deselect an open table on a unified document (the type "Create" makes, i.e. most new diagrams). Root cause:
useSelection.js's empty-click path (the shared block-shape selection) never reaches the whiteboard UI's ownclearSelection, which is what actually clearseditingCell/cellRange— a gap in the unified-canvas click-dispatch architecture shared by every whiteboard object type (stickies, lines, strokes), not just tables. Reproduces identically onmainbefore this branch, confirmed by stashing this branch's commits and rebuilding. Left unfixed here rather than risk a rushed change to click dispatch shared across the whole canvas — happy to take it as a fast follow-up if wanted.Everything else in #556's acceptance criteria is implemented and covered by tests below.
Test plan
yarn vitest run— 1765 tests passingyarn build/yarn lint— clean🤖 Generated with Claude Code